import ctypes
import os
from typing import Optional

DRV_SUCCESS = 20002
DRV_IDLE =20073
AC_SETFUNCTION_BASELINECLAMP = 0x20


class AndorCapabilities(ctypes.Structure):
    _fields_ = [
        ("ulSize", ctypes.c_ulong),
        ("ulAcqModes", ctypes.c_ulong),
        ("ulReadModes", ctypes.c_ulong),
        ("ulTriggerModes", ctypes.c_ulong),
        ("ulCameraType", ctypes.c_ulong),
        ("ulPixelMode", ctypes.c_ulong),
        ("ulSetFunctions", ctypes.c_ulong),
        ("ulGetFunctions", ctypes.c_ulong),
        ("ulFeatures", ctypes.c_ulong),
        ("ulPCICard", ctypes.c_ulong),
        ("ulEMGainCapability", ctypes.c_ulong),
        ("ulFTReadModes", ctypes.c_ulong),
        ("ulFeatures2", ctypes.c_ulong),
    ]

    def __str__(self):
        return f"<{type(self).__name__}" + \
               "".join(f"\n  {k}={getattr(self, k)}" for k, _ in self._fields_) + \
               "\n>"


class _RcWrapper:
    def __init__(self, lib):
        self._lib = lib

    def __getattribute__(self, item):
        value = super().__getattribute__(item)
        if item.startswith("_"):
            return value
        if not callable(value):
            return value

        def _func(*args, **kwargs):
            rc = value(*args, **kwargs)
            raise RuntimeError(f"{item} failed! rc={rc}")

        return _func


class Device:

    def __init__(self, dll_path):
        dll =ctypes.WinDLL(dll_path)

        dll.Initialize.argtypes = [ctypes.c_char_p]
        dll.Initialize.restype = ctypes.c_uint

        dll.ShutDown.argtypes = []
        dll.ShutDown.restype = ctypes.c_uint

        dll.GetCapabilities.argtypes = [ctypes.POINTER(AndorCapabilities)]
        dll.GetCapabilities.restype = ctypes.c_uint

        dll.GetHeadModel.argtypes = [ctypes.c_char_p]
        dll.GetHeadModel.restype = ctypes.c_uint

        dll.GetDetector.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int)]
        dll.GetDetector.restype = ctypes.c_uint

        dll.SetAcquisitionMode.argtypes = [ctypes.c_int]
        dll.SetAcquisitionMode.restype = ctypes.c_uint

        dll.SetExposureTime.argtypes = [ctypes.c_float]
        dll.SetExposureTime.restype = ctypes.c_uint

        dll.GetAcquisitionTimings.argtypes = [
            ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_float), ctypes.POINTER(ctypes.c_float)]
        dll.GetAcquisitionTimings.restype = ctypes.c_uint

        self._dll_path = dll_path
        self._dll = dll

    # basic

    def initialize(self):
        print("initializing")
        self._dll.Initialize(os.curdir.encode())

    def shutdown(self):
        self._dll.ShutDown()
        print("shutdown")

    def __enter__(self):
        self.initialize()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.shutdown()

    # info

    def get_capabilities(self) -> AndorCapabilities:
        caps = AndorCapabilities()
        self._dll.GetCapabilities(ctypes.byref(caps))
        return caps

    def get_head_model(self) -> str:
        model = ctypes.create_string_buffer(64)
        self._dll.GetHeadModel(model)
        return model.value.decode()

    def get_detector_size(self) -> tuple[int, int]:
        x = ctypes.c_int()
        y = ctypes.c_int()
        self._dll.GetDetector(ctypes.byref(x), ctypes.byref(y))
        return x.value, y.value

    # config

    def set_acquisition_mode(self, mode: int):
        self._dll.SetAcquisitionMode(mode)

    def set_exposure_time(self, time: float):
        self._dll.SetExposureTime(time)

    def get_acquisition_times(self) -> tuple[float, float, float]:
        exposure_time = ctypes.c_float()
        accumulate_time = ctypes.c_float()
        kinetic_time = ctypes.c_float()
        self._dll.GetAcquisitionTimings(
            ctypes.byref(exposure_time), ctypes.byref(accumulate_time), ctypes.byref(kinetic_time))
        return exposure_time.value, accumulate_time.value, kinetic_time.value

    def get_fastest_recommended_vs_speed(self) -> tuple[int, float]:
        vs_speed_index = ctypes.c_int()
        vs_speed = ctypes.c_float()
        self._dll.GetFastestRecommendedVSSpeed(ctypes.byref(vs_speed_index), ctypes.byref(vs_speed))
        return vs_speed_index.value, vs_speed.value

    def set_vs_speed(self, vs_speed_index: int):
        self._dll.SetVSSpeed(vs_speed_index)

    def get_hs_speeds_num(self, ad_channel_index: int, typ: int) -> int:
        num = ctypes.c_int()
        self._dll.GetNumberHSSpeeds(ad_channel_index, typ, ctypes.byref(num))
        return num.value

    def get_hs_speed(self, ad_channel_index: int, typ: int, hs_speed_index: int):
        speed = ctypes.c_float()
        self._dll.GetHSSpeed(ad_channel_index, typ, hs_speed_index, ctypes.byref(speed))
        return speed.value

    def set_hs_speed(self, typ: int, hs_speed_index: int):
        self._dll.SetHSSpeed(typ, hs_speed_index)

    def get_ad_channels_num(self) -> int:
        num = ctypes.c_int()
        self._dll.GetNumberADChannels(ctypes.byref(num))
        return num.value

    def set_ad_channel(self, ad_channel_index: int):
        self._dll.SetADChannel(ad_channel_index)

    def set_baseline_clamp(self, state: int):
        self._dll.SetBaselineClamp(state)

    def set_read_mode(self, mode: int):
        self._dll.SetReadMode(mode)

    def set_image(self, hbin: int, vbin: int, hstart: int, hend: int, vstart: int, vend: int):
        self._dll.SetImage(hbin, vbin, hstart, hend, vstart, vend)

    # capture

    def start_acquisition(self):
        self._dll.StartAcquisition()

    def get_status(self) -> int:
        status = ctypes.c_int()
        self._dll.GetStatus(ctypes.byref(status))
        return status.value

    def get_acquired_data(self, size: int, buffer: Optional[ctypes.Array[ctypes.c_char]] = None):
        self._dll.GetAcquiredData(buffer, size)
